State Exposure
ACTION REQUIRED
Legacynet is deprecating dry runs on October 10, 2025. Migrating to state exposure is now urgent for all processes running on Legacynet. This guide shows you how to expose your process state for direct HTTP access, providing dramatically better performance than dry runs.
HyperBEAM enables direct HTTP access to process state, eliminating the need for costly dryrun
calls. This feature dramatically improves performance for web frontends and data services that need to read process data.
The Patch Device
The ~patch@1.0
device is the mechanism that allows AO processes to make parts of their internal state readable via direct HTTP GET requests.
How State Exposure Works
State exposure follows a simple four-step pattern:
- Process Logic: From your process (e.g., in Lua or WASM), send an outbound message to the
~patch@1.0
device. - Patch Message Format: The message must include
device
andcache
tags.luaSend({ Target = ao.id, device = 'patch@1.0', cache = { mydatakey = MyValue } })
- HyperBEAM Execution: HyperBEAM's
dev_patch
module processes this message, mapping the key-value pairs from thecache
table to a URL path. - HTTP Access: The exposed data is then immediately available via a standard HTTP GET request to the process's endpoint.HyperBEAM
GET /<process-id>~process@1.0/compute/cache/<mydatakey>
Initial State Sync (Optional)
To make data available immediately on process creation, you can patch its initial state. A common pattern is to use a flag to ensure this sync only runs once, as shown in this example for a token's Balances
and TotalSupply
.
-- Place this logic at the top level of your process script,
-- outside of specific handlers, so it runs on load.
Balances = { token1 = 100, token2 = 200 } -- A table of balances
TotalSupply = 1984 -- A single total supply value
-- 1. Initialize Flag:
-- Initializes a flag if it doesn't exist.
InitialSync = InitialSync or 'INCOMPLETE'
-- 2. Check Flag:
-- Checks if the sync has already run.
if InitialSync == 'INCOMPLETE' then
-- 3. Patch State:
-- The `Send` call patches the state, making it available at endpoints like:
-- /cache/balances
-- /cache/totalsupply
Send({ device = 'patch@1.0', cache = { balances = Balances, totalsupply = TotalSupply } })
-- 4. Update Flag:
-- Updates the flag to prevent the sync from running again.
InitialSync = 'COMPLETE'
print("Initial state sync complete. Balances and TotalSupply patched.")
end
This pattern makes essential data queryable upon process creation, boosting application responsiveness.
Example (Lua in aos
)
This handler exposes a currentstatus
key that can be read via HTTP after the PublishData
action is called.
-- In your process code (e.g., loaded via .load)
Handlers.add(
"PublishData",
Handlers.utils.hasMatchingTag("Action", "PublishData"),
function (msg)
local dataToPublish = "Some important state: " .. math.random()
-- Expose 'currentstatus' key under the 'cache' path
Send({ device = 'patch@1.0', cache = { currentstatus = dataToPublish } })
print("Published data to /cache/currentstatus")
end
)
Avoiding Key Conflicts
Keys in the cache
table become URL path segments. To avoid conflicts with reserved HyperBEAM paths, use descriptive, specific keys. Avoid using reserved keywords such as:
now, compute, state, info, test
For instance, prefer a key like myappstate
over a generic key like state
.
WARNING
HTTP paths are case-insensitive. While the patch
device stores keys with case sensitivity (e.g., MyKey
vs mykey
), HTTP access to paths like the following is ambiguous and may lead to unpredictable results.
To prevent conflicts, always use lowercase keys in your cache
table (e.g., mykey
, usercount
).
GET /<process-id>~process@1.0/cache/mykey
Key Points
- Path Structure: Data is exposed at a path structured like this, where
<key>
is a key from yourcache
table:HyperBEAM/<process-id>~process@1.0/cache/<key>
- Data Types: Basic data types like strings and numbers work best. Complex objects may require serialization.
compute
vsnow
: Accessing patched data can be done via two main paths:HyperBEAMTheGET /<process-id>~process@1.0/compute/cache/... GET /<process-id>~process@1.0/now/cache/...
compute
endpoint serves the last known value quickly, whilenow
may perform additional computation to get the most recent state.- Read-Only Exposure: Patching is for efficient reads and does not replace your process's core state management logic.
Using the patch
device enables efficient, standard HTTP access to your process state, seamlessly connecting decentralized logic with web applications.
Patching User-Owned Processes
If your application spawns processes that are owned by users (not your application), you'll need to provide a way for users to patch their own processes. A common example is marketplace applications where each user has their own process instance.
Implementation Strategy
You can create UI components that allow users to update their processes. Here's an example approach:
import { message, createDataItemSigner } from "@permaweb/aoconnect";
async function patchUserProcess(processId) {
// User must sign this message themselves
const signer = createDataItemSigner(window.arweaveWallet);
const messageId = await message({
process: processId,
signer,
tags: [{ name: "Action", value: "UpdateToPatch" }],
});
return messageId;
}
In your process code, add a handler that users can trigger:
Handlers.add(
"UpdateToPatch",
Handlers.utils.hasMatchingTag("Action", "UpdateToPatch"),
function(msg)
-- Only allow the process owner to update
if msg.From ~= ao.id then
print("Only the process owner can update to use patch")
return
end
-- Add your patch logic here
Send({
device = 'patch@1.0',
cache = {
-- Add the state you want to expose
}
})
print("Process updated to use state patching")
end
)
This allows users to maintain ownership of their processes while still benefiting from HyperBEAM's performance improvements.
For a complete guide on implementing this pattern, see User-Owned Processes.
Next Steps
With state exposure configured, you can now add dynamic reads to compute values on-the-fly without modifying your process state.